// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fbl/auto_call.h>
#include <fbl/unique_fd.h>
#include <fbl/unique_ptr.h>
#include <zircon/types.h>

#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

#include <utility>

#include "xdc-init.h"

// TODO(jocelyndang): investigate issue with larger buffer sizes.
static constexpr uint32_t BUFFER_SIZE = 8 * 1024;
static constexpr uint32_t DEFAULT_STREAM_ID = 1;

typedef struct {
  off_t file_size;
} file_header_t;

static void usage(const char* prog_name) {
  printf("usage:\n");
  printf("%s [options]\n", prog_name);
  printf("\nOptions\n");
  printf(
      "  -i <stream id>  : ID of stream to transfer over, must be positive. Defaults to 1.\n"
      "  -f <filename>   : Name of file to write to or read from.\n"
      "  -d              : Download from xdc. This is the default if no mode is specified.\n"
      "  -u              : Upload to xdc.\n");
}

// Reads the file header from the xdc device and stores it in out_file_header.
static zx_status_t read_file_header(const fbl::unique_fd& xdc_fd, file_header_t* out_file_header) {
  unsigned char* buf = reinterpret_cast<unsigned char*>(out_file_header);

  ssize_t res;
  size_t total_read = 0;
  size_t len = sizeof(file_header_t);
  while ((total_read < len) &&
         ((res = read(xdc_fd.get(), buf + total_read, len - total_read)) != 0)) {
    if (res < 0) {
      printf("Fatal read error: %s\n", strerror(errno));
      return ZX_ERR_IO;
    }
    total_read += res;
  }
  if (total_read != len) {
    fprintf(stderr, "Malformed file header, only read %lu bytes, want %lu\n", total_read, len);
    return ZX_ERR_BAD_STATE;
  }
  return ZX_OK;
}

// Writes the file header to the xdc device and also stores it in out_file_header.
static zx_status_t write_file_header(const fbl::unique_fd& file_fd, fbl::unique_fd& xdc_fd,
                                     file_header_t* out_file_header) {
  struct stat s;
  if (fstat(file_fd.get(), &s) < 0) {
    fprintf(stderr, "could not get size of file, err: %s\n", strerror(errno));
    return ZX_ERR_IO;
  }
  file_header_t file_header = {.file_size = s.st_size};
  unsigned char* buf = reinterpret_cast<unsigned char*>(&file_header);
  ssize_t res = write(xdc_fd.get(), buf, sizeof(file_header));
  if (sizeof(res) != sizeof(file_header)) {
    fprintf(stderr, "Fatal write err: %s\n", strerror(errno));
    return ZX_ERR_IO;
  }
  ZX_DEBUG_ASSERT(out_file_header != nullptr);
  memcpy(out_file_header, &file_header, sizeof(file_header));
  return ZX_OK;
}

// Reads from the src_fd and writes to the dest_fd until src_len bytes has been written,
// or a fatal error occurs while reading or writing.
static zx_status_t transfer(fbl::unique_fd& src_fd, off_t src_len, fbl::unique_fd& dest_fd) {
  printf("Transferring file of size %jd bytes.\n", (uintmax_t)src_len);

  fbl::unique_ptr<unsigned char*[]> buf(new unsigned char*[BUFFER_SIZE]);
  ssize_t res;
  off_t total_read = 0;
  while ((total_read < src_len) && ((res = read(src_fd.get(), buf.get(), BUFFER_SIZE)) != 0)) {
    if (res < 0) {
      fprintf(stderr, "Fatal read error: %s\n", strerror(errno));
      return ZX_ERR_IO;
    }
    total_read += res;

    ssize_t buf_len = res;
    ssize_t total_written = 0;
    while (total_written < buf_len) {
      ssize_t res = write(dest_fd.get(), buf.get() + total_written, buf_len - total_written);
      if (res < 0) {
        fprintf(stderr, "Fatal write err: %s\n", strerror(errno));
        return ZX_ERR_IO;
      }
      total_written += res;
    }
  }
  return ZX_OK;
}

int main(int argc, char** argv) {
  auto print_usage = fbl::MakeAutoCall([argv]() { usage(argv[0]); });

  const char* filename = nullptr;
  uint32_t stream_id = DEFAULT_STREAM_ID;
  bool download = true;

  int opt;
  while ((opt = getopt(argc, argv, "i:f:du")) != -1) {
    switch (opt) {
      case 'i':
        if (sscanf(optarg, "%u", &stream_id) != 1) {
          fprintf(stderr, "Failed to parse stream id: \"%s\"\n", optarg);
          return -1;
        }
        if (stream_id == 0) {
          fprintf(stderr, "Stream ID must be positive\n");
          return -1;
        }
        break;
      case 'f':
        filename = optarg;
        break;
      case 'd':
        download = true;
        break;
      case 'u':
        download = false;
        break;
      default:
        fprintf(stderr, "Invalid option\n");
        return -1;
    }
  }
  if (!filename) {
    fprintf(stderr, "No file specified\n");
    return -1;
  }
  // Finished parsing the arguments without error.
  print_usage.cancel();

  fbl::unique_fd xdc_fd;
  zx_status_t status = configure_xdc(stream_id, &xdc_fd);
  if (status != ZX_OK) {
    return -1;
  }

  int file_flags = download ? (O_RDWR | O_CREAT) : O_RDONLY;
  fbl::unique_fd file_fd(open(filename, file_flags, 0666));
  if (!file_fd) {
    fprintf(stderr, "Failed to open \"%s\", err %s\n", filename, strerror(errno));
    return -1;
  }

  fbl::unique_fd src_fd;
  fbl::unique_fd dest_fd;

  file_header_t file_header;
  if (download) {
    if (read_file_header(xdc_fd, &file_header) != ZX_OK) {
      return -1;
    }
    src_fd = std::move(xdc_fd);
    dest_fd = std::move(file_fd);
  } else {
    if (write_file_header(file_fd, xdc_fd, &file_header) != ZX_OK) {
      return -1;
    }
    src_fd = std::move(file_fd);
    dest_fd = std::move(xdc_fd);
  }

  status = transfer(src_fd, file_header.file_size, dest_fd);
  if (status != ZX_OK) {
    return -1;
  }
  return 0;
}
